﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.RegularExpressions;

namespace AppConfigVerifier
{
	public class InclusionExclusionRuleSetCreator : RuleSetCreator
	{
		class RuleTokenComparer : IComparer<RuleToken>
		{
			#region IComparer<RuleToken> Members

			public int Compare(RuleToken x, RuleToken y)
			{
				return new LocationComparer().Compare(x.Location, y.Location);
			}

			#endregion
		}

		//enum RuleType
		//{
		//    Include,
		//    Exclude,
		//    IncludeAssembly,
		//    ExcludeAssembly
		//}
		class RuleToken
		{
			public Location Location
			{
				get;
				set;
			}
			public Rule Rule
			{
				get;
				set;
			}
		}

		class RuleTokensExtension
		{
			readonly Regex includeExclude = new Regex(@"<!--\s*TypeVerification\s*(?<ExclusionType>Include|Exclude|IncludeAssembly|ExcludeAssembly)\s*(:\s*(?<TypeRegex>.*))?-->",
						  RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);

			//readonly Regex excludes = new Regex(@"<!--\s*Exclude\s*(:\s*(?<TypeRegex>.*))?-->", RegexOptions.Compiled);
			readonly GlobalInfo context;

			public RuleTokensExtension(GlobalInfo context)
			{
				this.context = context;
			}

			Regex ToRegex(string pattern, int index)
			{
				Regex regex = null;
				try
				{
					regex = new Regex(pattern);
				}
				catch(ArgumentException ex)
				{
					context.Log.Send("Cannot parse the regex "
						+ pattern + ", the corresponding rule will be ignored : "
						+ ex.Message, index, index + pattern.Length - 1, LogType.Warning);
				}
				return regex;
			}

			List<RuleToken> tokens;

			public IEnumerable<RuleToken> Tokens
			{
				get
				{
					if(tokens == null)
					{
						tokens = ScanContent();
					}
					return tokens;
				}
			}

			List<RuleToken> ScanContent()
			{
				string content = context.Content;

				var matches = includeExclude.Matches(content).OfType<Match>();
				IEnumerable<RuleToken> allTokens = matches.Select(m => CreateToken(content, m));
				//var excludesToken = excludes.Matches(content).OfType<Match>()
				//.Select(m => CreateToken(RuleType.Exclude, content, m));

				List<RuleToken> tokens = allTokens.Where(token => token.Rule != null).ToList();
				tokens.Sort(new RuleTokenComparer());
				return tokens;
			}

			RuleToken CreateToken(string content, Match m)
			{
				var token = new RuleToken();
				token.Location = Location.GetLocation(m.Index, content);
				//token.Rule = GetRule(m.Groups["ExclusionType"].Value,  token.Location);
				Group typeRegexGroup = m.Groups["TypeRegex"];

				token.Rule = GetRule(m, m.Groups["ExclusionType"], typeRegexGroup, token.Location);
				//ToRegex(typeRegexGroup.Value.Trim(), typeRegexGroup.Index);
				//if(token.TypeRegex == null)
				//    token.IsInvalid = true;

				return token;
			}

			Rule GetRule(Match ruleMatch, Group type, Group value, Location ruleLocation)
			{
				string valueStr = value.Value.Trim();
				bool hasValue = value.Success;
				Regex regex = null;
				if(hasValue)
				{
					regex = ToRegex(valueStr, value.Index);
					if(regex == null)
					{
						return null;
					}
				}

				if(type.Value.Equals("Include", StringComparison.InvariantCultureIgnoreCase))
				{
					return new IncludeExcludeRule(regex, ruleLocation, true);
				}

				if(type.Value.Equals("Exclude", StringComparison.InvariantCultureIgnoreCase))
				{
					return new IncludeExcludeRule(regex, ruleLocation, false);
				}

				if(type.Value.Equals("ExcludeAssembly", StringComparison.InvariantCultureIgnoreCase))
				{
					if(!hasValue)
					{
						context.Log.Send("ExcludeAssembly should have an assembly specified",
							ruleMatch.Index, LogType.Error);
						return null;
					}
					AssemblyName assemblyName = Utility.StrToAssemblyName(valueStr);
					if(assemblyName == null)
					{
						context.Log.Send(valueStr + " is not an assemblyName", value.Index, LogType.Error);
						return null;
					}
					return new IncludeExcludeAssemblyRule(assemblyName, false);
				}

				if(type.Value.Equals("IncludeAssembly", StringComparison.InvariantCultureIgnoreCase))
				{
					if(!hasValue)
					{
						context.Log.Send("IncludeAssembly should have an assembly specified",
							ruleMatch.Index, LogType.Error);
						return null;
					}
					AssemblyName assemblyName = Utility.StrToAssemblyName(valueStr);
					if(assemblyName == null)
					{
						context.Log.Send(valueStr + " is not an assemblyName", value.Index, LogType.Error);
						return null;
					}
					return new IncludeExcludeAssemblyRule(assemblyName, true);
				}

				throw new NotSupportedException(type.Value);
			}
		}

		class InclusionValidationRule : Rule
		{
			InclusionExclusionRuleSetCreator creator;

			public InclusionValidationRule(InclusionExclusionRuleSetCreator creator)
			{
				this.creator = creator;
			}

			public override void Apply(LocalContext context)
			{
				if(context.GetExtension<InclusionExclusionExtension>().IsIncluded)
				{
					Validate(context);
				}
			}

			void Validate(LocalContext context)
			{
				using(var loader = new AssemblyLoader())
				{
					TypeDefinition typeDefinition;
					try
					{
						typeDefinition = Utility.GetTypeDefinition(context.TypeName);
					}
					catch(FileLoadException)
					{
						context.Log.Send("Cannot parse assembly name in " + context.TypeName, LogType.Error);
						return;
					}


					if(typeDefinition.AssemblyName == null)
					{
						SearchTypeInDefaultAssemblies(typeDefinition.TypeName, context, loader);
						return;
					}

					int indexOfAssemblyName = context.Token.TypeName.IndexOf(',') + 1;
					while(context.Token.TypeName[indexOfAssemblyName] == ' ')
					{
						indexOfAssemblyName++;
					}
					AssemblyName bestMatch = null;
					AssemblyName assemblyName = typeDefinition.AssemblyName;

					Utility.AssemblyNameMatchResult missMatchResult = null;
					foreach(AssemblyName knownName in creator.KnownNames)
					{
						var matchResultLocal = assemblyName.IsSubsetOf(knownName);
						if(!matchResultLocal.Success && (missMatchResult == null || missMatchResult.Mistmatch.MismatchType < matchResultLocal.Mistmatch.MismatchType))
							missMatchResult = matchResultLocal;
						if(matchResultLocal.Success)
						{
							bestMatch = knownName;
							break;
						}
					}
					if(bestMatch == null)
					{
						string hintError = String.Empty;
						if(missMatchResult != null)
						{
							switch(missMatchResult.Mistmatch.MismatchType)
							{
								case Utility.MismatchType.Name:
									break;
								case Utility.MismatchType.Version:
									hintError = "Version";
									break;
								case Utility.MismatchType.PublicToken:
									hintError = "PublicKeyToken";
									break;
								case Utility.MismatchType.Culture:
									hintError = "Culture";
									break;
								case Utility.MismatchType.ProcessorArchitecture:
									hintError = "ProcessorArchitecture";
									break;
							}
						}
						if(hintError != String.Empty)
						{
							hintError = ", did you mean " + hintError + "=\"" + missMatchResult.Mistmatch.OtherAssemblyValue + "\"" + " instead of " + hintError + "=\"" + missMatchResult.Mistmatch.ThisValue + "\" ?";
						}
						context.Log.Send("Assembly : " + assemblyName.ToString()
								+ " not referenced in the project" + hintError, indexOfAssemblyName,
											 LogType.Error);
						return;
					}

					bool typeIsFound;
					try
					{
						string assemblyPath = creator.assembliesDictionary[bestMatch];
						typeIsFound = loader.HasType(assemblyPath, typeDefinition.TypeName);
					}
					catch(FileNotFoundException)
					{
						context.Log.Send(bestMatch
							+ " is referenced by the project but not found at "
							+ creator.assembliesDictionary[bestMatch],
							indexOfAssemblyName, LogType.Error);
						return;
					}


					if(!typeIsFound)
					{
						context.Log.Send(typeDefinition.TypeName + " was not found inside "
							+ assemblyName.ToString(), LogType.Error);
						return;
					}
				}
			}


			void SearchTypeInDefaultAssemblies(string typeName, LocalContext context, AssemblyLoader loader)
			{
				bool assemblyFound = false;
				foreach(string defaultReference in creator.DefaultReferences)
				{
					try
					{
						assemblyFound = loader.HasType(defaultReference, typeName);
						if(assemblyFound)
						{
							break;
						}
					}
					catch(FileNotFoundException)
					{
						context.Log.Send("Default assembly not found : "
							+ defaultReference, LogType.Warning);
					}
				}
				if(!assemblyFound)
				{
					context.Log.Send("No type " + typeName
						+ " found in default assemblies", LogType.Error);
				}
			}
		}


		readonly Dictionary<AssemblyName, String> assembliesDictionary = new Dictionary<AssemblyName, string>();
		readonly Dictionary<AssemblyName, String> defaultAssembliesDictionary = new Dictionary<AssemblyName, string>();

		public InclusionExclusionRuleSetCreator(string[] references, string[] defaultReferences)
		{
			FillDictionary(references.Union(defaultReferences).ToArray(), assembliesDictionary);
			FillDictionary(defaultReferences, defaultAssembliesDictionary);
		}

		void FillDictionary(string[] references, Dictionary<AssemblyName, String> dictionary)
		{
			foreach(string reference in references)
			{
				AssemblyName name = null;
				try
				{
					name = AssemblyName.GetAssemblyName(reference);
				}
				catch(BadImageFormatException) //Not Managed
				{
					/* Ignore. */
				}
				if(name != null)
				{
					dictionary.Add(name, reference);
				}
			}
		}

		public string[] References
		{
			get
			{
				return assembliesDictionary.Values.ToArray();
			}
		}

		public string[] DefaultReferences
		{
			get
			{
				return defaultAssembliesDictionary.Values.ToArray();
			}
		}

		public AssemblyName[] DefaultKnownNames
		{
			get
			{
				return defaultAssembliesDictionary.Keys.ToArray();
			}
		}

		public AssemblyName[] KnownNames
		{
			get
			{
				return assembliesDictionary.Keys.ToArray();
			}
		}

		public override IEnumerable<Rule> CreateRules(LocalContext context)
		{
			var extension = context.GlobalInfo.GetExtension<RuleTokensExtension>();
			List<Rule> rules = extension.Tokens.Select(token => token.Rule).ToList();
			rules.Add(new InclusionValidationRule(this));
			return rules;
		}
	}
}
